{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "overview",
   "metadata": {},
   "source": [
    "# Strands Agents with AgentCore Memory (Long term memory)\n",
    "\n",
    "## Introduction\n",
    "\n",
    "This tutorial demonstrates how to build an **intelligent customer support agent** using Strands agents integrated with AgentCore Memory via hooks. We'll focus on Long term memory for customer interaction history, remembering purchase details, and provides personalized support based on previous conversations and user preferences.\n",
    "\n",
    "### Tutorial Details\n",
    "\n",
    "| Information         | Details                                                                          |\n",
    "|:--------------------|:---------------------------------------------------------------------------------|\n",
    "| Tutorial type       | Long term Conversational                                                         |\n",
    "| Agent type          | Customer Support                                                                 |\n",
    "| Agentic Framework   | Strands Agents                                                                   |\n",
    "| LLM model           | Anthropic Claude Sonnet 3.7                                                      |\n",
    "| Tutorial components | AgentCore Semantic and User Preferences Memory Extraction, Hooks for storing and retrieving Memory              |\n",
    "| Example complexity  | Intermediate                                                                     |\n",
    "\n",
    "You'll learn to:\n",
    "- Set up AgentCore Memory with Long term strategies\n",
    "- Create memory hooks for automatic storage and retrieval\n",
    "- Build a customer support agent with persistent memory\n",
    "- Handle customer issues with context from previous interactions\n",
    "\n",
    "### Scenario Context\n",
    "n this example, we will build a **Customer Support Use Case**. The agent will remember customer context, including order history, preferences, and previous issues, enabling more personalized and effective support. Conversations with customers are automatically stored using memory hooks, ensuring that important details are never lost. By employing multiple memory strategies such as semantic, and User Preference — the agent can capture a wide range of relevant information. This setup allows the agent to resolve issues with full awareness of the customer's history and preferences. Additionally, the agent is integrated with web search capabilities, making it easy to provide up-to-date product information and troubleshooting guidance as needed.\n",
    "\n",
    "## Architecture\n",
    "\n",
    "<div style=\"text-align:left\">\n",
    "    <img src=\"architecture.png\" width=\"65%\" />\n",
    "</div>\n",
    "\n",
    "\n",
    "## Prerequisites\n",
    "\n",
    "To execute this tutorial you will need:\n",
    "- Python 3.10+\n",
    "- AWS credentials with Amazon Bedrock AgentCore Memory permissions\n",
    "- Amazon Bedrock AgentCore SDK"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "setup",
   "metadata": {},
   "source": [
    "## Step 1: Install Dependencies and Setup\n",
    "Let's begin importing all the necessary libraries and defining the clients to make this notebook work."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0d1b0211-7b59-4789-8d7e-1e85ea8273aa",
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install -qr requirements.txt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "imports",
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging\n",
    "import json\n",
    "from typing import Dict\n",
    "from datetime import datetime\n",
    "from botocore.exceptions import ClientError\n",
    "\n",
    "# Setup logging\n",
    "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s - %(levelname)s - %(message)s\")\n",
    "logger = logging.getLogger(\"customer-support\")\n",
    "\n",
    "# Import required modules\n",
    "from strands import Agent, tool\n",
    "from strands.hooks import AfterInvocationEvent, HookProvider, HookRegistry, MessageAddedEvent\n",
    "from ddgs import DDGS"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "73fd1583",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Configuration - Replace with the correct values\n",
    "REGION = \"us-west-2\"\n",
    "CUSTOMER_ID = \"customer_001\"\n",
    "SESSION_ID = f\"support_{datetime.now().strftime('%Y%m%d%H%M%S')}\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "step2",
   "metadata": {},
   "source": [
    "## Step 2: Create Memory Resource for Customer Support\n",
    "\n",
    "For customer support, we'll use multiple memory strategies:\n",
    "- **USER_PREFERENCE**: Captures customer preferences and behavior\n",
    "- **SEMANTIC**: Stores order facts and product information"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "create_memory",
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "from bedrock_agentcore.memory import MemoryClient\n",
    "from bedrock_agentcore.memory.constants import StrategyType\n",
    "\n",
    "# Initialize Memory Client\n",
    "client = MemoryClient(region_name=REGION)\n",
    "memory_name = \"CustomerSupportMemory\"\n",
    "\n",
    "# Define memory strategies for customer support\n",
    "strategies = [\n",
    "    {\n",
    "        StrategyType.USER_PREFERENCE.value: {\n",
    "            \"name\": \"CustomerPreferences\",\n",
    "            \"description\": \"Captures customer preferences and behavior\",\n",
    "            \"namespaces\": [\"support/customer/{actorId}/preferences\"]\n",
    "        }\n",
    "    },\n",
    "    {\n",
    "        StrategyType.SEMANTIC.value: {\n",
    "            \"name\": \"CustomerSupportSemantic\",\n",
    "            \"description\": \"Stores facts from conversations\",\n",
    "            \"namespaces\": [\"support/customer/{actorId}/semantic\"],\n",
    "        }\n",
    "    }\n",
    "]\n",
    "\n",
    "# Create memory resource\n",
    "try:\n",
    "    memory = client.create_memory_and_wait(\n",
    "        name=memory_name,\n",
    "        strategies=strategies,         # Define the memory strategies\n",
    "        description=\"Memory for customer support agent\",\n",
    "        event_expiry_days=90,          # Memories expire after 90 days\n",
    "    )\n",
    "    memory_id = memory['id']\n",
    "    logger.info(f\"✅ Created memory: {memory_id}\")\n",
    "except ClientError as e:\n",
    "    if e.response['Error']['Code'] == 'ValidationException' and \"already exists\" in str(e):\n",
    "        # If memory already exists, retrieve its ID\n",
    "        memories = client.list_memories()\n",
    "        memory_id = next((m['id'] for m in memories if m['id'].startswith(memory_name)), None)\n",
    "        logger.info(f\"Memory already exists. Using existing memory ID: {memory_id}\")\n",
    "except Exception as e:\n",
    "    # Handle any errors during memory creation\n",
    "    logger.info(f\"❌ ERROR: {e}\")\n",
    "    import traceback\n",
    "    traceback.print_exc()\n",
    "    # Cleanup on error - delete the memory if it was partially created\n",
    "    if memory_id:\n",
    "        try:\n",
    "            client.delete_memory_and_wait(memoryId=memory_id,max_wait = 300)\n",
    "            logger.info(f\"Cleaned up memory: {memory_id}\")\n",
    "        except Exception as cleanup_error:\n",
    "            logger.info(f\"Failed to clean up memory: {cleanup_error}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "96d5b9fd-0a3a-43a0-a688-169cd06e9491",
   "metadata": {},
   "source": [
    "Let's confirm if our memory contains the strategies we assigned"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d6354a27-900c-481a-8938-fbdd13ac2b16",
   "metadata": {},
   "outputs": [],
   "source": [
    "strategies = client.get_memory_strategies(memory_id)\n",
    "print(json.dumps(strategies, indent=2, default=str))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "step3",
   "metadata": {},
   "source": [
    "## Step 3: Create Agent Tools"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "support_tools",
   "metadata": {},
   "outputs": [],
   "source": [
    "from ddgs.exceptions import DDGSException, RatelimitException\n",
    "from ddgs import DDGS\n",
    "\n",
    "@tool\n",
    "def web_search(query: str, max_results: int = 3) -> str:\n",
    "    \"\"\"Search the web for product information, troubleshooting guides, or support articles.\n",
    "    \n",
    "    Args:\n",
    "        query: Search query for product info or troubleshooting\n",
    "        max_results: Maximum number of results to return\n",
    "    \n",
    "    Returns:\n",
    "        Search results with titles and snippets\n",
    "    \"\"\"\n",
    "    try:\n",
    "        results = DDGS().text(query, region=\"us-en\", max_results=max_results)\n",
    "        if not results:\n",
    "            return \"No search results found.\"\n",
    "        \n",
    "        formatted_results = []\n",
    "        for i, result in enumerate(results, 1):\n",
    "            formatted_results.append(f\"{i}. {result.get('title', 'No title')}\\n   {result.get('body', 'No description')}\")\n",
    "        \n",
    "        return \"\\n\".join(formatted_results)\n",
    "    except RatelimitException:\n",
    "        return \"Rate limit reached: Please try again after a short delay.\"\n",
    "    except DuckDuckGoSearchException as d:\n",
    "        return f\"Search Error: {d}\"\n",
    "    except Exception as e:\n",
    "        return f\"Search error: {str(e)}\"\n",
    "\n",
    "logger.info(\"✅ Web search tool ready\")\n",
    "\n",
    "@tool\n",
    "def check_order_status(order_number: str) -> str:\n",
    "    \"\"\"Check the status of a customer order.\n",
    "    \n",
    "    Args:\n",
    "        order_number: The order number to check\n",
    "    \n",
    "    Returns:\n",
    "        Order status information\n",
    "    \"\"\"\n",
    "    # Simulate order lookup\n",
    "    mock_orders = {\n",
    "        \"123456\": \"iPhone 15 Pro - Delivered on June 5, 2025\",\n",
    "        \"654321\": \"Sennheiser Headphones - Delivered on June 25, 2025, 1-year warranty active\",\n",
    "        \"789012\": \"Samsung Galaxy S23 - In transit, expected delivery on July 1, 2025\",\n",
    "    }\n",
    "    \n",
    "    return mock_orders.get(order_number, f\"Order {order_number} not found. Please verify the order number.\")\n",
    "\n",
    "logger.info(\"✅ Check Order Status tool ready\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "step4",
   "metadata": {},
   "source": [
    "## Step 4: Create Memory Hook Provider for Customer Support\n",
    "Hooks are special functions that run at specific points in an agent's execution lifecycle. Our custom hook provider will automatically manage customer support context by:\n",
    "- **Saving support interactions** after each response\n",
    "- **Retrieving and Injecting relevant context** from previous orders and preferences when processing new queries.\n",
    "\n",
    "This creates a seamless memory experience without manual management"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cd34c34b-81bb-419e-aecf-a6eda5ee8c61",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Helper function to get namespaces from memory strategies list\n",
    "def get_namespaces(mem_client: MemoryClient, memory_id: str) -> Dict:\n",
    "    \"\"\"Get namespace mapping for memory strategies.\"\"\"\n",
    "    strategies = mem_client.get_memory_strategies(memory_id)\n",
    "    return {i[\"type\"]: i[\"namespaces\"][0] for i in strategies}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "memory_hooks",
   "metadata": {},
   "outputs": [],
   "source": [
    "class CustomerSupportMemoryHooks(HookProvider):\n",
    "    \"\"\"Memory hooks for customer support agent\"\"\"\n",
    "    \n",
    "    def __init__(self, memory_id: str, client: MemoryClient, actor_id: str, session_id: str):\n",
    "        self.memory_id = memory_id\n",
    "        self.client = client\n",
    "        self.actor_id = actor_id\n",
    "        self.session_id = session_id\n",
    "        self.namespaces = get_namespaces(self.client, self.memory_id)\n",
    "\n",
    "    \n",
    "    def retrieve_customer_context(self, event: MessageAddedEvent):\n",
    "        \"\"\"Retrieve customer context before processing support query\"\"\"\n",
    "        messages = event.agent.messages\n",
    "        if messages[-1][\"role\"] == \"user\" and \"toolResult\" not in messages[-1][\"content\"][0]:\n",
    "            user_query = messages[-1][\"content\"][0][\"text\"]\n",
    "            \n",
    "            try:\n",
    "                # Retrieve customer context from all namespaces\n",
    "                all_context = []\n",
    "                \n",
    "                for context_type, namespace in self.namespaces.items():\n",
    "                    memories = self.client.retrieve_memories(\n",
    "                        memory_id=self.memory_id,\n",
    "                        namespace=namespace.format(actorId=self.actor_id),\n",
    "                        query=user_query,\n",
    "                        top_k=3\n",
    "                    )\n",
    "                    \n",
    "                    for memory in memories:\n",
    "                        if isinstance(memory, dict):\n",
    "                            content = memory.get('content', {})\n",
    "                            if isinstance(content, dict):\n",
    "                                text = content.get('text', '').strip()\n",
    "                                if text:\n",
    "                                    all_context.append(f\"[{context_type.upper()}] {text}\")\n",
    "                \n",
    "                # Inject customer context into the query\n",
    "                if all_context:\n",
    "                    context_text = \"\\n\".join(all_context)\n",
    "                    original_text = messages[-1][\"content\"][0][\"text\"]\n",
    "                    messages[-1][\"content\"][0][\"text\"] = (\n",
    "                        f\"Customer Context:\\n{context_text}\\n\\n{original_text}\"\n",
    "                    )\n",
    "                    logger.info(f\"Retrieved {len(all_context)} customer context items\")\n",
    "                    \n",
    "            except Exception as e:\n",
    "                logger.error(f\"Failed to retrieve customer context: {e}\")\n",
    "    \n",
    "    def save_support_interaction(self, event: AfterInvocationEvent):\n",
    "        \"\"\"Save support interaction after agent response\"\"\"\n",
    "        try:\n",
    "            messages = event.agent.messages\n",
    "            if len(messages) >= 2 and messages[-1][\"role\"] == \"assistant\":\n",
    "                # Get last customer query and agent response\n",
    "                customer_query = None\n",
    "                agent_response = None\n",
    "                \n",
    "                for msg in reversed(messages):\n",
    "                    if msg[\"role\"] == \"assistant\" and not agent_response:\n",
    "                        agent_response = msg[\"content\"][0][\"text\"]\n",
    "                    elif msg[\"role\"] == \"user\" and not customer_query and \"toolResult\" not in msg[\"content\"][0]:\n",
    "                        customer_query = msg[\"content\"][0][\"text\"]\n",
    "                        break\n",
    "                \n",
    "                if customer_query and agent_response:\n",
    "                    # Save the support interaction\n",
    "                    self.client.create_event(\n",
    "                        memory_id=self.memory_id,\n",
    "                        actor_id=self.actor_id,\n",
    "                        session_id=self.session_id,\n",
    "                        messages=[(customer_query, \"USER\"), (agent_response, \"ASSISTANT\")]\n",
    "                    )\n",
    "                    logger.info(\"Saved support interaction to memory\")\n",
    "                    \n",
    "        except Exception as e:\n",
    "            logger.error(f\"Failed to save support interaction: {e}\")\n",
    "    \n",
    "    def register_hooks(self, registry: HookRegistry) -> None:\n",
    "        \"\"\"Register customer support memory hooks\"\"\"\n",
    "        registry.add_callback(MessageAddedEvent, self.retrieve_customer_context)\n",
    "        registry.add_callback(AfterInvocationEvent, self.save_support_interaction)\n",
    "        logger.info(\"Customer support memory hooks registered\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "step5",
   "metadata": {},
   "source": [
    "### Step 5: Create Customer Support Agent"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "create_agent",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create memory hooks for customer support\n",
    "support_hooks = CustomerSupportMemoryHooks(\n",
    "    memory_id=memory_id,\n",
    "    client=client,\n",
    "    actor_id=CUSTOMER_ID,\n",
    "    session_id=SESSION_ID\n",
    ")\n",
    "\n",
    "# Create customer support agent\n",
    "support_agent = Agent(\n",
    "    hooks=[support_hooks],\n",
    "    tools=[web_search, check_order_status],\n",
    "    system_prompt=\"\"\"You are a helpful customer support agent with access to customer history and order information. \n",
    "    \n",
    "    Your role:\n",
    "    - Help customers with their orders, returns, and product issues\n",
    "    - Use customer context to provide personalized support\n",
    "    - Search for product information when needed\n",
    "    - Be empathetic and solution-focused\n",
    "    - Reference previous orders and preferences when relevant\n",
    "    \n",
    "    Always be professional, helpful, and aim to resolve customer issues efficiently.\"\"\"\n",
    ")\n",
    "\n",
    "print(\"✅ Customer support agent created with memory capabilities\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "step6",
   "metadata": {},
   "source": [
    "### Step 6: Seed Customer History\n",
    "\n",
    "Let's add some previous customer interactions to demonstrate memory functionality."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "seed_history",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Seed with previous customer interactions\n",
    "previous_interactions = [\n",
    "    (\"I bought a new iPhone 15 Pro on June 1st, 2025. Order number is 123456.\", \"USER\"),\n",
    "    (\"Thank you for your purchase! I can see your iPhone 15 Pro order #123456 was delivered successfully. How can I help you today?\", \"ASSISTANT\"),\n",
    "    (\"I also ordered Sennheiser headphones on June 20th. Order number 654321. They came with 1-year warranty.\", \"USER\"),\n",
    "    (\"Perfect! I have your Sennheiser headphones order #654321 on file with the 1-year warranty. Both your iPhone and headphones should work great together.\", \"ASSISTANT\"),\n",
    "    (\"I'm looking for a good laptop. I prefer ThinkPad models.\", \"USER\"),\n",
    "    (\"Great choice! ThinkPads are excellent for their durability and performance. Let me help you find the right model for your needs.\", \"ASSISTANT\")\n",
    "]\n",
    "\n",
    "# Save previous interactions\n",
    "try:\n",
    "    client.create_event(\n",
    "        memory_id=memory_id,\n",
    "        actor_id=CUSTOMER_ID,\n",
    "        session_id=\"previous_session\",\n",
    "        messages=previous_interactions\n",
    "    )\n",
    "    print(\"✅ Seeded customer history\")\n",
    "except Exception as e:\n",
    "    print(f\"⚠️ Error seeding history: {e}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "step7",
   "metadata": {},
   "source": [
    "#### Agent is ready to go. \n",
    "\n",
    "### Lets test Customer Support Scenarios"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "test1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test 1: Customer reports iPhone issue\n",
    "response1 = support_agent(\"My iPhone is running very slow and gets hot when charging. Can you help?\")\n",
    "print(f\"Support Agent: {response1}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "test2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test 2: Bluetooth connectivity issue\n",
    "response2 = support_agent(\"My iPhone won't connect to my Sennheiser headphones via Bluetooth. How do I fix this?\")\n",
    "print(f\"Support Agent: {response2}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "test3",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test 3: Check order status\n",
    "response3 = support_agent(\"Can you check the status of my recent orders?\")\n",
    "print(f\"Support Agent: {response3}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "test4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test 4: Product recommendation based on preferences\n",
    "response4 = support_agent(\"I'm still interested in buying a laptop. What ThinkPad models do you recommend?\")\n",
    "print(f\"Support Agent: {response4}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "step8",
   "metadata": {},
   "source": [
    "### Verify Customer Memory Storage"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "verify_memory",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Check stored customer memories\n",
    "print(\"\\n📚 Customer Memory Summary:\")\n",
    "print(\"=\" * 50)\n",
    "\n",
    "namespaces_dict = get_namespaces(client, memory_id)\n",
    "for context_type, namespace_template in namespaces_dict.items():\n",
    "    namespace = namespace_template.replace(\"{actorId}\", CUSTOMER_ID)\n",
    "    \n",
    "    try:\n",
    "        memories = client.retrieve_memories(\n",
    "            memory_id=memory_id,\n",
    "            namespace=namespace,\n",
    "            query=\"customer orders and preferences\",\n",
    "            top_k=3\n",
    "        )\n",
    "        \n",
    "        print(f\"\\n{context_type.upper()} ({len(memories)} items):\")\n",
    "        for i, memory in enumerate(memories, 1):\n",
    "            if isinstance(memory, dict):\n",
    "                content = memory.get('content', {})\n",
    "                if isinstance(content, dict):\n",
    "                    text = content.get('text', '')[:150] + \"...\"\n",
    "                    print(f\"  {i}. {text}\")\n",
    "                    \n",
    "    except Exception as e:\n",
    "        print(f\"Error retrieving {context_type} memories: {e}\")\n",
    "\n",
    "print(\"\\n\" + \"=\" * 50)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0199ac1b",
   "metadata": {},
   "source": [
    "#### Customer Support Tutorial completed! 🎉\n",
    "Key takeaways:\n",
    "- Memory hooks automatically manage customer context across support sessions\n",
    "- Multi-strategy memory captures orders, preferences, and facts from conversations\n",
    "- Agents can provide personalized support based on customer history\n",
    "- Tools can be integrated for order lookup and web search functionality\n",
    "- Customer support becomes more efficient with persistent memory"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cleanup",
   "metadata": {},
   "source": [
    "## Clean Up\n",
    "\n",
    "### Optional: Delete Memory Resource"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cleanup_code",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Uncomment to delete the memory resource\n",
    "# try:\n",
    "#     client.delete_memory_and_wait(memory_id=memory_id)\n",
    "#     print(f\"✅ Deleted memory resource: {memory_id}\")\n",
    "# except Exception as e:\n",
    "#     print(f\"Error deleting memory: {e}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "conda_pytorch_p310",
   "language": "python",
   "name": "conda_pytorch_p310"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
